/*
 * Copyright (c) 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.codegen.test.codegen.use;

import java.lang.annotation.ElementType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;

import io.helidon.common.types.Annotation;
import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypeNames;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent;
import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsArrayContainingInOrder.arrayContaining;
import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize;
import static org.junit.jupiter.api.Assertions.fail;

public class CodegenValidationTest {
    private static final String CLASS_NAME = "io.helidon.codegen.test.codegen.use.TriggerType__Generated";
    private static Class<?> clazz;
    private static Object instance;

    @BeforeAll
    public static void setUpClass() {
        try {
            clazz = Class.forName(CLASS_NAME);
        } catch (ClassNotFoundException e) {
            fail("Class " + CLASS_NAME + " should have been code generated by TestCodegenExtension");
        }
        try {
            instance = clazz.getConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            fail("Class " + CLASS_NAME + " should have an accessible constructor", e);
        }
    }

    @Test
    void testClassModifiers() throws ReflectiveOperationException {
        String modifiers = (String) clazz.getMethod("classModifiers")
                .invoke(instance);

        assertThat(modifiers, containsString("final"));
    }

    @Test
    void testMethodModifiers() throws ReflectiveOperationException {
        String modifiers = (String) clazz.getMethod("methodModifiers")
                .invoke(instance);

        assertThat(modifiers, containsString("final"));
        assertThat(modifiers, containsString("synchronized"));
    }

    @Test
    void testFieldModifiers() throws ReflectiveOperationException {
        String modifiers = (String) clazz.getMethod("fieldModifiers")
                .invoke(instance);

        assertThat(modifiers, containsString("transient"));
        assertThat(modifiers, containsString("volatile"));
    }

    @Test
    public void testGeneratedClass() {
        CrazyAnnotation annotation = clazz.getAnnotation(CrazyAnnotation.class);
        assertThat(annotation, notNullValue());
        validateAnnotation(annotation);
    }

    @Test
    public void testGeneratedConstant() {
        Field field;
        try {
            field = clazz.getDeclaredField("ANNOTATION");
        } catch (NoSuchFieldException e) {
            fail("Class " + CLASS_NAME + " should contain a constant field \"ANNOTATION\", generated by TestCodegenExtension");
            return;
        }

        Annotation annotation;
        try {
            annotation = (Annotation) field.get(null);
        } catch (IllegalAccessException e) {
            fail("Failed to get field ANNOTATION from the generated class " + CLASS_NAME, e);
            return;
        }

        assertThat(annotation, notNullValue());
        assertThat(annotation.typeName(), is(TypeName.create(CrazyAnnotation.class)));
        assertThat(annotation.stringValue("stringValue"), optionalValue(is("value1")));
        assertThat(annotation.booleanValue("booleanValue"), optionalValue(is(true)));
        assertThat(annotation.longValue("longValue"), optionalValue(is(49L)));
        assertThat(annotation.doubleValue("doubleValue"), optionalValue(is(49D)));
        assertThat(annotation.intValue("intValue"), optionalValue(is(49)));
        assertThat(annotation.byteValue("byteValue"), optionalValue(is((byte) 49)));
        assertThat(annotation.charValue("charValue"), optionalValue(is('x')));
        assertThat(annotation.shortValue("shortValue"), optionalValue(is((short) 49)));
        assertThat(annotation.floatValue("floatValue"), optionalValue(is(49F)));
        assertThat(annotation.typeValue("classValue"), optionalValue(is(TypeNames.STRING)));
        assertThat(annotation.typeValue("typeValue"), optionalValue(is(TypeNames.STRING)));
        assertThat(annotation.enumValue("enumValue", ElementType.class),
                   optionalValue(is(ElementType.FIELD)));
        assertThat(annotation.annotationValue("annotationValue"), optionalPresent());

        // lists
        assertThat(annotation.stringValues("lstring"),
                   optionalValue(is(List.of("value1", "value2"))));
        assertThat(annotation.booleanValues("lboolean")
                , optionalValue(is(List.of(true, false))));
        assertThat(annotation.longValues("llong"),
                   optionalValue(is(List.of(49L, 50L))));
        assertThat(annotation.doubleValues("ldouble"),
                   optionalValue(is(List.of(49D, 50D))));
        assertThat(annotation.intValues("lint"),
                   optionalValue(is(List.of(49, 50))));
        assertThat(annotation.byteValues("lbyte"),
                   optionalValue(is(List.of((byte) 49, (byte) 50))));
        assertThat(annotation.charValues("lchar"),
                   optionalValue(is(List.of('x', 'y'))));
        assertThat(annotation.shortValues("lshort"),
                   optionalValue(is(List.of((short) 49, (short) 50))));
        assertThat(annotation.floatValues("lfloat"),
                   optionalValue(is(List.of(49F, 50F))));
        assertThat(annotation.typeValues("lclass"),
                   optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT))));
        assertThat(annotation.typeValues("ltype"),
                   optionalValue(is(List.of(TypeNames.STRING, TypeNames.BOXED_INT))));
        assertThat(annotation.enumValues("lenum", ElementType.class),
                   optionalValue(is(List.of(ElementType.FIELD, ElementType.MODULE))));
        assertThat(annotation.annotationValues("lannotation"), optionalPresent());
        assertThat(annotation.stringValues("emptyList"), optionalValue(is(List.of())));
        assertThat(annotation.stringValues("singletonList"), optionalValue(is(List.of("value"))));
    }

    @Test
    public void testGeneratedField() {
        Field field;
        try {
            field = clazz.getDeclaredField("field");
        } catch (NoSuchFieldException e) {
            fail("Class " + CLASS_NAME + " should contain a field \"field\", generated by TestCodegenExtension");
            return;
        }

        assertThat("Field type should be String", field.getType(), sameInstance(String.class));
        assertThat("Field should be private", Modifier.isPrivate(field.getModifiers()));
        assertThat("Field should not be final", !Modifier.isFinal(field.getModifiers()));
        assertThat("Field should not be static", !Modifier.isStatic(field.getModifiers()));

        CrazyAnnotation annotation = field.getAnnotation(CrazyAnnotation.class);
        assertThat(annotation, notNullValue());
        validateAnnotation(annotation);
    }

    @Test
    public void testGeneratedConstructor() {
        Constructor<?> ctr;
        try {
            ctr = clazz.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            fail("Class " + CLASS_NAME + " should contain a no-argument constructor, generated by TestCodegenExtension");
            return;
        }

        assertThat("Constructor should be public", Modifier.isPublic(ctr.getModifiers()));

        CrazyAnnotation annotation = ctr.getAnnotation(CrazyAnnotation.class);
        assertThat(annotation, notNullValue());
        validateAnnotation(annotation);
    }

    @Test
    public void testGeneratedMethod() {
        Method method;
        try {
            method = clazz.getDeclaredMethod("method");
        } catch (NoSuchMethodException e) {
            fail("Class " + CLASS_NAME + " should contain a method \"method\", generated by TestCodegenExtension");
            return;
        }

        assertThat("Method should be public", Modifier.isPublic(method.getModifiers()));
        assertThat("Method should not be final", !Modifier.isFinal(method.getModifiers()));
        assertThat("Method should not be static", !Modifier.isStatic(method.getModifiers()));
        assertThat("Method return type should be void", method.getReturnType(), sameInstance(void.class));

        CrazyAnnotation annotation = method.getAnnotation(CrazyAnnotation.class);
        assertThat(annotation, notNullValue());
        validateAnnotation(annotation);
    }

    private void validateAnnotation(CrazyAnnotation annotation) {
        // single values
        assertThat(annotation.stringValue(), is("value1"));
        assertThat(annotation.booleanValue(), is(true));
        assertThat(annotation.longValue(), is(49L));
        assertThat(annotation.doubleValue(), is(49D));
        assertThat(annotation.intValue(), is(49));
        assertThat(annotation.byteValue(), is((byte) 49));
        assertThat(annotation.charValue(), is('x'));
        assertThat(annotation.shortValue(), is((short) 49));
        assertThat(annotation.floatValue(), is(49F));
        assertThat(annotation.classValue(), sameInstance(String.class));
        assertThat(annotation.typeValue(), sameInstance(String.class));
        assertThat(annotation.enumValue(), is(ElementType.FIELD));
        assertThat(annotation.annotationValue().value(), arrayContaining(ElementType.CONSTRUCTOR));

        // arrays
        assertThat(annotation.lstring(), arrayContaining("value1", "value2"));

        assertThat("Should be same boolean array, but is: " + Arrays.toString(annotation.lboolean()),
                   Arrays.equals(annotation.lboolean(), new boolean[] {true, false}));
        assertThat("Should be same long array, but is: " + Arrays.toString(annotation.llong()),
                   Arrays.equals(annotation.llong(), new long[] {49L, 50L}));
        assertThat("Should be same double array, but is: " + Arrays.toString(annotation.ldouble()),
                   Arrays.equals(annotation.ldouble(), new double[] {49D, 50D}));
        assertThat("Should be same int array, but is: " + Arrays.toString(annotation.lint()),
                   Arrays.equals(annotation.lint(), new int[] {49, 50}));
        assertThat("Should be same byte array, but is: " + Arrays.toString(annotation.lbyte()),
                   Arrays.equals(annotation.lbyte(), new byte[] {(byte) 49, (byte) 50}));
        assertThat("Should be same char array, but is: " + Arrays.toString(annotation.lchar()),
                   Arrays.equals(annotation.lchar(), new char[] {'x', 'y'}));
        assertThat("Should be same short array, but is: " + Arrays.toString(annotation.lshort()),
                   Arrays.equals(annotation.lshort(), new short[] {(short) 49, (short) 50}));
        assertThat("Should be same float array, but is: " + Arrays.toString(annotation.lfloat()),
                   Arrays.equals(annotation.lfloat(), new float[] {49F, 50F}));
        assertThat(annotation.lclass(), arrayContaining(String.class, Integer.class));
        assertThat(annotation.ltype(), arrayContaining(String.class, Integer.class));
        assertThat(annotation.lenum(), arrayContaining(ElementType.FIELD, ElementType.MODULE));
        assertThat(annotation.lannotation(), arrayWithSize(2));
        assertThat(annotation.emptyList(), arrayWithSize(0));
        assertThat(annotation.singletonList(), arrayContaining("value"));
    }
}
